package spring;

import java.io.FileNotFoundException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import spring.beanDefinition.BeanDefinition;
import spring.beanDefinition.PropertyEntry;
import spring.beanDefinition.Value;
import spring.beanDefinition.ValueType;
import spring.xmlParser.ParserType;

public class GenericXmlApplicationContext {
	
	private XmlBeanDefinitionReader reader;
	
	public GenericXmlApplicationContext(XmlBeanDefinitionReader reader){
		this.reader = reader;
	}
	
	public void setValidation(boolean validating){
		reader.setValidating(validating);
	}
	
	public void setParserType(ParserType parserType){
		reader.setParserType(parserType);
	}
	
	public BeanFactory getBeanFactory(final String XmlApplicationContextFileLocation)
			throws FileNotFoundException{
		
		BeanFactory factory = new BeanFactory() {
			
			Map<String, BeanDefinition> beansDefinition = reader.loadBeansDefinition(XmlApplicationContextFileLocation);
			
			Map<String, Object> beans = new HashMap<>();
			
			Logger logger = Logger.getLogger(BeanFactory.class.getName());
			
			//@SuppressWarnings("unchecked")
			@Override
			public <T> T getBean(String id, Class<T> type) {
				if(!beans.containsKey(id)){
					try {
						if(!addBean(id, type)){
							logger.log(Level.SEVERE, "Bean " + id + " couldn't have been created. Returning null.");
							return null;
						}
					} catch (ClassNotFoundException | InstantiationException
							| IllegalAccessException | IllegalArgumentException
							| InvocationTargetException e) {
						logger.log(Level.INFO, e.getMessage());
					}
				}
				
				return tryCast(beans.get(id), type);
			}
			
			private <T> boolean addBean(String id, Class<T> type) 
					throws ClassNotFoundException,
					InstantiationException,
					IllegalAccessException,
					IllegalArgumentException,
					InvocationTargetException{
				BeanDefinition beanDefinition = beansDefinition.get(id);
				
				Class<?> clazz = Class.forName(beanDefinition.clazz);
				
				int numOfArguments = beanDefinition.constructorArgs.size();
				List<Object> arguments = new ArrayList<>(numOfArguments);
				Constructor<?>[] constructors = clazz.getConstructors();
				for(Constructor<?> constructor : constructors){
					if(constructor.getParameterTypes().length != numOfArguments)
						continue;
					
					Class<?>[] constructorArguments = constructor.getParameterTypes();
					arguments.clear();
					for(int i = 0; i < numOfArguments; i++){
						Value argumentValue = beanDefinition.constructorArgs.get(i);
						if(argumentValue.type == ValueType.VALUE){
							Object argument = definePrimitive(argumentValue.value, constructorArguments[i]);
							if(argument == null)
								break;
							else{
								arguments.add(argument);
							}
						}
						else if(argumentValue.type == ValueType.REF){
							arguments.add(getBean(argumentValue.value, constructorArguments[i]));
						}
					}
					
					//check whether all arguments were cast to specific type
					//invariant : arguments.size() <= numOfArguments
					if(arguments.size() == numOfArguments){
						Object newBean = constructor.newInstance(arguments.toArray());
						
						setProperties(newBean, beanDefinition.properties);
						
						beans.put(id, newBean);
						return true;
					}
				}
				return false;
			}
			
			/**
			 * If argument class is Boolean, true will be returned if the string argument 
			 * is not null and is equal, ignoring case, to the string "true". In other cases,
			 * false will be returned
			 */
			private Object definePrimitive(String value, Class<?> argumentClass){
				try{
					return argumentClass.cast(Boolean.valueOf(value));
				} catch(ClassCastException e) {
					logger.log(Level.INFO, e.getMessage());
				}
				try{
					return argumentClass.cast(Double.valueOf(value));
				} catch(NumberFormatException | ClassCastException e) {
					logger.log(Level.INFO, e.getMessage());
				}
				try{
					return argumentClass.cast(Integer.valueOf(value));
				} catch(NumberFormatException | ClassCastException e) {
					logger.log(Level.INFO, e.getMessage());
				}
				try{
					return argumentClass.cast(value);
				} catch (ClassCastException e){
					logger.log(Level.INFO, e.getMessage());
				}
				return null;
			}
			
			private void setProperties(Object bean, List<PropertyEntry> properties){
				for(PropertyEntry propertyEntry : properties){
					Method setMethod = getSetterMethod(bean, propertyEntry);
					if(setMethod == null) {
						logger.log(Level.SEVERE, propertyEntry.name + 
								" wasn't set: no setter method has been found");
						return;
					}
					Class<?> parameterClass = setMethod.getParameterTypes()[0];
					Object parameter = null;
					if(propertyEntry.value.type.equals(ValueType.VALUE)){
						parameter = definePrimitive(propertyEntry.value.value, parameterClass);
						if(parameter == null){
							logger.log(Level.SEVERE, propertyEntry.name +
									" wasn't set: setter method parameter is not appropriate");
							return;
						}
					}
					else if(propertyEntry.value.type.equals(ValueType.REF)){
						parameter = getBean(propertyEntry.value.value, Object.class);
					}
					try {
						setMethod.invoke(bean, parameter);
					} catch (IllegalAccessException | IllegalArgumentException
							| InvocationTargetException e) {
						logger.log(Level.SEVERE, propertyEntry.name +
								" wasn't set : " + e.getMessage());
					}
				}
			}
			
			/**
			 * @return Method in bean that has specific name with one argument
			 * 		   Null if no appropriate method has been found	
			 */
			private Method getSetterMethod(Object bean, PropertyEntry propertyEntry){
				String setterName = "set" +
						propertyEntry.name.substring(0, 1).toUpperCase() +
						propertyEntry.name.substring(1);
				Method[] methods = bean.getClass().getMethods();
				for(int i = 0; i < methods.length; i++){
					Method currMethod = methods[i];
					if(currMethod.getName().equals(setterName) && currMethod.getParameterTypes().length == 1){
						return currMethod;
					}
				}
				return null;
			}
			
			@SuppressWarnings("unchecked")
			private <T> T tryCast(Object bean, Class<T> to){
				if(to.isAssignableFrom(bean.getClass()))
					return (T) bean;
				else{
					logger.log(Level.SEVERE, "Unable to cast to " + to.getName() + ". Returning null.");
					return null;
				}
			}
		};
		
		return factory;
	}
}
